package com.majun.service.impl;

import com.majun.constant.RedisPreConstant;
import com.majun.constant.RsaConstant;
import com.majun.service.RsaService;
import com.majun.utils.RSAUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.Base64Utils;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.crypto.Cipher;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.concurrent.TimeUnit;

/**
 * rsa加密
 *
 * @author majun
 * @date 2025/05/14
 */
@Service
@Slf4j
public class RsaServiceImpl implements RsaService {
    @Resource
    private RedisTemplate redisTemplate;

    /**
     * RSA算法
     */
    private static final String RSA_ALGORITHM = "RSA";

    /**
     * 公私钥长度
     */
    private static final Integer KEY_LENGTH = 2048;

    /**
     * RSA算法公钥
     */
    private static final String RSA_PUBLIC_KEY = "RSAPublicKey";

    /**
     * RSA算法私钥
     */
    private static final String RSA_PRIVATE_KEY = "RSAPrivateKey";

    /**
     * 初始化
     *
     * @throws Exception
     */
    @PostConstruct
    public void init() throws Exception {
        log.info("RsaServiceImpl init start");
        Provider provider = new org.bouncycastle.jce.provider.BouncyCastleProvider();
        Security.addProvider(provider);
        SecureRandom random = new SecureRandom();
        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", provider);
        generator.initialize(RsaConstant.KEY_SIZE, random);
        KeyPair keyPair = generator.generateKeyPair();
        //将公钥和私钥存放，登录时会不断请求获取公钥，我们可以将其放到缓存中，而不放入数据库了
        //我在想，这个是不是有必要存放到Redis，在分布式场景中？
        //貌似有些必要，万一获取到的pubkey是server1中的，拿着server1的pubkey去server2去解密？
        storeRsa(keyPair);
        log.info("RsaServiceImpl init end");
    }

    /**
     * 将RSA存入缓存
     *
     * @param keyPair
     */
    private void storeRsa(KeyPair keyPair) {
        //1、存储公钥key
        String publicRedisKey = getRedisKey(RsaConstant.PUBLIC_KEY);
        PublicKey publicKey = keyPair.getPublic();
        //公钥字符串
        String publicKeyStr = new String(Base64.encodeBase64(publicKey.getEncoded()));
        redisTemplate.opsForValue().set(publicRedisKey, publicKeyStr, 1, TimeUnit.DAYS);

        //2、存储私钥key
        String privateRedisKey = getRedisKey(RsaConstant.PRIVATE_KEY);
        PrivateKey privateKey = keyPair.getPrivate();
        //私钥字符串
        String privateKeyStr = new String(Base64.encodeBase64(privateKey.getEncoded()));
        redisTemplate.opsForValue().set(privateRedisKey, privateKeyStr, 1, TimeUnit.DAYS);
    }

    /**
     * @param publicKey
     * @return {@link String }
     */
    private String getRedisKey(String publicKey) {
        return new StringBuilder()
                .append(RedisPreConstant.RSA)
                .append(publicKey)
                .toString();
    }

    /**
     * 从字符串中加载公钥
     *
     * @return {@link RSAPublicKey }
     * @throws Exception
     */
    public RSAPublicKey loadPublicKeyByStr() throws Exception {
        try {
            //公钥数据字符串
            String publicKeyStr = getPublicKey();
            // 使用Base64编码将数据加密公钥转换为字节数组
            byte[] publicKeyBytes = Base64Utils.decodeFromString(publicKeyStr);
            // 使用数据加密公钥字符数组创建一个X509EncodedKeySpec（密钥规范：公钥的ASN.1编码的一种表示形式）实例
            KeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyBytes);
            // 创建KeyFactory（JCE框架中的一个类）实例，用于转换公钥和私钥
            KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
            // 从x509EncodedKeySpec（密钥规范：公钥的ASN.1编码的一种表示形式）中生成一个公钥
            // 调用generatePublic方法使用之前创建的X509EncodedKeySpec对象来生成公钥。
            RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(x509EncodedKeySpec);
            return publicKey;
        } catch (NoSuchAlgorithmException e) {
            throw new Exception("无此算法");
        } catch (InvalidKeySpecException e) {
            throw new Exception("公钥非法");
        } catch (NullPointerException e) {
            throw new Exception("公钥数据为空");
        }
    }

    /**
     * 从字符串中加载私钥
     *
     * @return {@link RSAPrivateKey }
     * @throws Exception
     */
    public RSAPrivateKey loadPrivateKeyByStr() throws Exception {
        try {
            //私钥数据字符串
            String privateKeyStr = getPrivateKey();
            // 使用Base64编码将数据加密私钥转换为字节数组
            byte[] privateBytes = Base64Utils.decodeFromString(privateKeyStr);
            // 使用数据解密私钥字符数组创建一个PKCS8EncodedKeySpec（密钥规范：私钥的ASN.1编码的一种表示形式）实例
            KeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateBytes);
            // 创建KeyFactory（JCE框架中的一个类）实例，用于转换公钥和私钥
            KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
            // 从pkcs8EncodedKeySpec（密钥规范：私钥的ASN.1编码的一种表示形式）中生成一个私钥
            RSAPrivateKey privateKey = (RSAPrivateKey) keyFactory.generatePrivate(pkcs8EncodedKeySpec);
            return privateKey;
        } catch (NoSuchAlgorithmException e) {
            throw new Exception("无此算法");
        } catch (InvalidKeySpecException e) {
            throw new Exception("私钥非法");
        } catch (NullPointerException e) {
            throw new Exception("私钥数据为空");
        }
    }


    /**
     * 私钥解密(解密前台公钥加密的密文)
     *
     * @param encryptText 公钥加密的数据
     * @return 私钥解密出来的数据
     * @throws Exception e
     */
    @Override
    public String decryptWithPrivate(String encryptText) throws Exception {
        if (StringUtils.isBlank(encryptText)) {
            return null;
        }
        byte[] en_byte = Base64.decodeBase64(encryptText.getBytes());
        Provider provider = new org.bouncycastle.jce.provider.BouncyCastleProvider();
        Security.addProvider(provider);
        Cipher ci = Cipher.getInstance("RSA/ECB/PKCS1Padding", provider);
        PrivateKey privateKey = loadPrivateKeyByStr();
        ci.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] res = ci.doFinal(en_byte);
        return new String(res);
    }

    /**
     * 公钥加密
     *
     * @param plaintext 明文内容
     * @return byte[]
     * @throws Exception
     */
    @Override
    public String encrypt(String plaintext) throws Exception {
        String encode = URLEncoder.encode(plaintext, StandardCharsets.UTF_8.name());
        RSAPublicKey rsaPublicKey = loadPublicKeyByStr();

        // 获取公钥字节数组
        byte[] publicKeyBytes = rsaPublicKey.getEncoded();
        // 使用Base64编码将公钥字节数组转换为字符串
        String publicKeyString = Base64Utils.encodeToString(publicKeyBytes);
        String encryptString = RSAUtil.encryptByPublicKey(encode, publicKeyString);
        return encryptString;

    }

    /**
     * 私钥解密
     *
     * @param cipherText 加密后的字节数组
     * @return 解密后的数据
     * @throws Exception
     */
    @Override
    public String decrypt(String cipherText) throws Exception {
        RSAPrivateKey rsaPrivateKey = loadPrivateKeyByStr();
        // 创建Cipher实例，用于加密解密
        // 创建一个Cipher实例，它是用于加密或解密数据的对象。Cipher类提供了加密和解密功能，并支持许多不同的加密算法。
        // 在这里，getInstance 方法中传入了keyFactory.getAlgorithm()[获取与指定密钥工厂相关联的算法名称。]，它用于获取与指定算法关联的 Cipher 实例。
        Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
        // 初始化Cipher实例，设置解密模式并指定要使用的私钥
        // 在初始化过程中，指定加密模式为 ENCRYPT_MODE，并传入了之前生成的公钥 generatePublicKey。
        cipher.init(Cipher.DECRYPT_MODE, rsaPrivateKey);
        // 使用Base64编码将需要解密数据转换为字节数组
        byte[] needDecryptBytes = Base64Utils.decodeFromString(cipherText);
        // 解密为字节数组
        byte[] decryptBytes = cipher.doFinal(needDecryptBytes);
        // 使用Base64编码将加密后的数组转换为字符串
        return Base64Utils.encodeToString(decryptBytes);
    }

    /**
     * 获取公钥
     *
     * @return {@link String }
     * @throws Exception
     */
    @Override
    public String getPublicKey() throws Exception {
        //1、获取redis key
        String publicRedisKey = getRedisKey(RsaConstant.PUBLIC_KEY);
        //2、获取公钥字符串
        String publicKeyStr = (String) redisTemplate.opsForValue().get(publicRedisKey);
        if (StringUtils.isNotBlank(publicKeyStr)) {
            log.info("RsaServiceImpl getPublicKey publicKeyStr:{}", publicKeyStr);
            return publicKeyStr;
        }
        //3、初始化
        init();
        //4、重新获取公钥字符串
        return getPublicKey();
    }

    /**
     * 获取私钥
     *
     * @return {@link String }
     * @throws Exception
     */
    public String getPrivateKey() throws Exception {
        //1、获取redis key
        String privateRedisKey = getRedisKey(RsaConstant.PRIVATE_KEY);
        //2、获取私钥数据字符串
        String privateKeyStr = (String) redisTemplate.opsForValue().get(privateRedisKey);
        if (StringUtils.isNotBlank(privateKeyStr)) {
            log.info("RsaServiceImpl getPrivateKey privateKeyStr:{}", privateKeyStr);
            return privateKeyStr;
        }
        //3、初始化
        init();
        //4、重新获取公钥字符串
        return getPrivateKey();
    }
}
